/* @flow */

import config from "../config";
import { warn } from "./debug";
import { set } from "../observer/index";
import { unicodeRegExp } from "./lang";
import { nativeWatch, hasSymbol } from "./env";

import { ASSET_TYPES, LIFECYCLE_HOOKS } from "shared/constants";

import {
  extend,
  hasOwn,
  camelize,
  toRawType,
  capitalize,
  isBuiltInTag,
  isPlainObject,
} from "shared/util";

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const strats = config.optionMergeStrategies;

/**
 * Options with restrictions
 */
//el的合并
//判断只允许vue实例才拥有el属性，其他子类构造器不允许有el属性
//（默认配置）就是有孩子用孩子，没孩子用爸爸。
if (process.env.NODE_ENV !== "production") {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      // 只允许vue实例才拥有el属性，其他子类构造器不允许有el属性
      warn(
        `option "${key}" can only be used during instance ` +
          "creation with the `new` keyword."
      );
    }
    return defaultStrat(parent, child);
  };
}

/**
 * 递归合并两个数据对象，以to为主，to里面不存在则取from里面的，如过存在就是自己的
 * Helper that recursively merges two data objects together.
 */
function mergeData(to: Object, from: ?Object): Object {
  if (!from) return to;
  let key, toVal, fromVal;

  const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from);

  for (let i = 0; i < keys.length; i++) {
    key = keys[i];
    // in case the object is already observed...
    if (key === "__ob__") continue;
    toVal = to[key];
    fromVal = from[key];
    if (!hasOwn(to, key)) {
      // 目标对象中没有这个属性时，赋值
      set(to, key, fromVal);
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal);
    }
  }
  return to;
}

/**
 * Data
 * 先判断是否存在vm实例。没有vm的是子组件，就是存在父子之间的关系
 * 不存在vm(父子之间的关系)
 *  对data选项进行规范校验，保证它的类型是一个函数而不是对象。
 *  子类不存在，用父类的data。父类不存在用子类的data。子类和父类均存在data选项我们需要返回一个函数，这个函数返回这两个对象合并的结果。（以子类为主，子类不存在的进行添加）
 *  这里不需要检查parentVal是不是函数，因为它只有是函数才可以通过之前的合并。
 * 存在vm(vue实例)
 *  返回一个函数。当实例中传递data选项时，将实例的data对象和Vm构造函数上的data属性选项合并（以实例为主，实例不存在，构造函数存在的增加）。当实例中不传递data时，默认返回Vm构造函数上的data属性选项
 *
 */
export function mergeDataOrFn(
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  //子父类
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      // 子类不存在data选项，则合并结果为父类data选项
      return parentVal;
    }
    if (!parentVal) {
      // 父类不存在data选项，则合并结果为子类data选项
      return childVal;
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn() {
      // data选项在父类和子类同时存在的情况下返回的是一个函数
      // 子类实例和父类实例，分别将子类和父类实例中data函数执行后返回的对象传递给mergeData函数做数据合并
      return mergeData(
        typeof childVal === "function" ? childVal.call(this, this) : childVal,
        typeof parentVal === "function" ? parentVal.call(this, this) : parentVal
      );
    };
  } else {
    // Vue实例
    // vue构造函数实例对象
    return function mergedInstanceDataFn() {
      // instance merge
      const instanceData =
        typeof childVal === "function" ? childVal.call(vm, vm) : childVal;
      const defaultData =
        typeof parentVal === "function" ? parentVal.call(vm, vm) : parentVal;
      if (instanceData) {
        // 当实例中传递data选项时，将实例的data对象和Vm构造函数上的data属性选项合并
        return mergeData(instanceData, defaultData);
      } else {
        // 当实例中不传递data时，默认返回Vm构造函数上的data属性选项
        return defaultData;
      }
    };
  }
}

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // vm代表是否为Vue创建的实例，否则是子父类的关系
    if (childVal && typeof childVal !== "function") {
      // 必须保证子类的data类型是一个函数而不是一个对象
      process.env.NODE_ENV !== "production" &&
        warn(
          'The "data" option should be a function ' +
            "that returns a per-instance value in component " +
            "definitions.",
          vm
        );

      return parentVal;
    }
    return mergeDataOrFn(parentVal, childVal);
  }

  return mergeDataOrFn(parentVal, childVal, vm); // vue实例时需要传递vm作为函数的第三个参数
};

/**
 * Hooks and props are merged as arrays.
 * 
 * mergeHook是生命周期钩子合并的策略，简单的对代码进行总结，钩子函数的合并原则是：

当子类不存在钩子选项时，则以父类选项返回，数组形式。
如果父类不存在钩子选项，子类存在时，则以数组形式返回子类钩子选项。
如果子类和父类都拥有相同钩子选项，则将子类选项和父类选项合并。
子父合并时，是将子类选项放在数组的末尾，这样在执行钩子时，永远是父类选项优先于子类选项执行。
 */

function mergeHook(
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
      ? childVal
      : [childVal]
    : parentVal;
  return res ? dedupeHooks(res) : res;
}
// 防止多个组件实例钩子选项相互影响
function dedupeHooks(hooks) {
  const res = [];
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i]);
    }
  }
  return res;
}

LIFECYCLE_HOOKS.forEach((hook) => {
  strats[hook] = mergeHook;
});

/**
 * Assets
 * component directive filter
 * 如果存在父类就一父类为原型构建一个对象，否则已空构建一个对象，然后把子的extend进这个对象
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 */
function mergeAssets(
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null);
  if (childVal) {
    process.env.NODE_ENV !== "production" &&
      assertObjectType(key, childVal, vm);
    return extend(res, childVal);
  } else {
    return res;
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + "s"] = mergeAssets;
});

/**
 * Watchers.
 *对于 watch ，最后它会变成一个数组，这个数组里面注册着父子中定义的所有同属性的回调函数。而且执行顺序是父亲先孩子后。
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 * 没有 childVal 的话, 以 parentVal 为参数创建一个对象后返回
 * 检查 parentVal 如果没有值则返回 childVal
 * 对于watch选项的合并，最终和父类选项合并成数组，并且数组的选项成员，可以是回调函数，选项对象，或者函数名。
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined;
  if (childVal === nativeWatch) childVal = undefined;
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null);
  if (process.env.NODE_ENV !== "production") {
    // 有 childVal 的话呢，检查一下是不是对象类型
    assertObjectType(key, childVal, vm);
  }
  // 检查 parentVal 如果没有值则返回 childVal
  if (!parentVal) return childVal;
  const ret = {};
  extend(ret, parentVal);
  for (const key in childVal) {
    let parent = ret[key]; // 获取 parent 的值
    const child = childVal[key]; // 获取 child 的值
    // 如果 parent 有值并且不是数组的话, 把它变成数组
    if (parent && !Array.isArray(parent)) {
      parent = [parent];
    }
    // ret[key] 的值由 parent 的值来决定，如果 parent 有值的话，那么就再拼接上 child 的值
    // 如果 parent 没有值的话，那么就用 child 的值，不是数组转成数组即可
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child)
      ? child
      : [child];
  }
  return ret;
};

/**
 * Other object hashes.
 * 如果父类不存在选项，则返回子类选项，子类父类都存在时，用子类选项去覆盖父类选项。
 */
strats.props =
  strats.methods =
  strats.inject =
  strats.computed =
    function (
      parentVal: ?Object,
      childVal: ?Object,
      vm?: Component,
      key: string
    ): ?Object {
      if (childVal && process.env.NODE_ENV !== "production") {
        assertObjectType(key, childVal, vm);
      }
      if (!parentVal) return childVal;
      const ret = Object.create(null);
      extend(ret, parentVal);
      if (childVal) extend(ret, childVal);
      return ret;
    };
strats.provide = mergeDataOrFn;

/**
 * Default strategy.
 * // 用户自定义选项策略
 * 子配置存在则取子配置，不存在则取父配置，即用子去覆盖父
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal;
};

/**
 * // components规范检查函数
 */
function checkComponents(options: Object) {
  for (const key in options.components) {
    // 遍历components对象，对每个属性值校验。
    validateComponentName(key);
  }
}

export function validateComponentName(name: string) {
  if (
    !new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)
    // 正则判断检测是否为非法的标签，例如数字开头
  ) {
    warn(
      'Invalid component name: "' +
        name +
        '". Component names ' +
        "should conform to valid custom element name in html5 specification."
    );
  }
  // 不能使用Vue自身自定义的组件名，如slot, component,不能使用html的保留标签，如 h1, svg等
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      "Do not use built-in or reserved HTML elements as component " +
        "id: " +
        name
    );
  }
}

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 */
/**
 * Vue的官方文档规定了props选项的书写形式有两种，分别是
    数组形式 { props: ['a', 'b', 'c'] },
    带校验规则的对象形式 {props:{a:[String,Number]}} 或者 { props: { a: { type: 'String', default: 'prop校验' } }} 从源码上看，两种形式最终都会转换成对象的形式。
    props: {
        a:{
            type: xxx
        }
    }
 
    * @param {*} options 
 * @param {*} vm 
 * @returns 
 */
function normalizeProps(options: Object, vm: ?Component) {
  debugger;
  const props = options.props;
  if (!props) return;
  const res = {};
  let i, val, name;
  // 数组
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === "string") {
        name = camelize(val);
        // 默认将数组形式的props转换为对象形式。
        res[name] = { type: null };
      } else if (process.env.NODE_ENV !== "production") {
        // 规则：保证是字符串
        warn("props must be strings when using array syntax.");
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val) ? val : { type: val };
    }
  } else if (process.env.NODE_ENV !== "production") {
    // 非数组，非对象则判定props选项传递非法
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
        `but got ${toRawType(props)}.`,
      vm
    );
  }
  options.props = res;
}

/**
 * Normalize all injections into Object-based format
 */

/**
 * inject: ['foo']
   inject: {foo: { default: 'xxx' }}
   inject: {foo}
 * 上述3者规范化为：
    inject: {
        foo: {
            from: 'foo',
            default: 'xxx'  //如果有默认的值就有default属性
        }
    }
 * 
 * 
 * 
 * @param {*} options 
 * @param {*} vm 
 * @returns 
 */

function normalizeInject(options: Object, vm: ?Component) {
  const inject = options.inject;
  if (!inject) return;
  const normalized = (options.inject = {});
  if (Array.isArray(inject)) {
    //如果inject是个s数组['foo']
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] };
    }
  } else if (isPlainObject(inject)) {
    //如果传入的是一个对象
    for (const key in inject) {
      const val = inject[key];
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val };
    }
  } else if (process.env.NODE_ENV !== "production") {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
        `but got ${toRawType(inject)}.`,
      vm
    );
  }
}

/** 指令对象形式
 * directives: {
        focus: {
            // 指令的定义
            bind:(){},
            update
            inserted: function (el) {
                el.focus()
            },
            componentUpdated
            unbind
        }
    }

函数的写法会在bind,update钩子中触发相同的行为，并且不关心其他钩子。这个行为就是定义的函数。因此在对directives进行规范化时，针对函数的写法会将行为赋予bind,update钩子。
指令函数形式
    directives: {
        'color-swatch': function(el, binding) {
            el.style.backgroundColor = binding.value
        }
    }
 * Normalize raw function directives into object format.
 */
function normalizeDirectives(options: Object) {
  const dirs = options.directives;
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key];
      // 函数简写同样会转换成对象的形式
      if (typeof def === "function") {
        dirs[key] = { bind: def, update: def };
      }
    }
  }
}

function assertObjectType(name: string, value: any, vm: ?Component) {
  if (!isPlainObject(value)) {
    warn(
      `Invalid value for option "${name}": expected an Object, ` +
        `but got ${toRawType(value)}.`,
      vm
    );
  }
}

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== "production") {
    checkComponents(child); //components规范检验;
  }

  if (typeof child === "function") {
    child = child.options;
  }

  //规范化props
  normalizeProps(child, vm);
  //将用户书写的inject进行规范化
  normalizeInject(child, vm);
  //规范化directives
  normalizeDirectives(child);

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  // 针对extends扩展的子类构造器
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm);
      }
    }
  }

  /**
   * mergeField 函数，它不是简单的把属性从一个对象里复制到另外一个对象里，而是根据被合并的不同的选项有着不同的合并策略。
   * 例如，对于data有data的合并策略，即该文件中的strats.data函数；对于watch有watch的合并策略，即该文件中的strats.watch函数等等。这就是设计模式中非常典型的策略模式。
   *
   */
  const options = {};
  let key;
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }

  function mergeField(key) {
    // 拿到各个选择指定的选项配置，如果没有则用默认的配置
    const strat = strats[key] || defaultStrat;
    // 执行各自的合并策略
    options[key] = strat(parent[key], child[key], vm, key);
  }
  debugger;
  return options;
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 */
export function resolveAsset(
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== "string") {
    return;
  }
  const assets = options[type];
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id];
  const camelizedId = camelize(id);
  if (hasOwn(assets, camelizedId)) return assets[camelizedId];
  const PascalCaseId = capitalize(camelizedId);
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId];
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
  if (process.env.NODE_ENV !== "production" && warnMissing && !res) {
    warn("Failed to resolve " + type.slice(0, -1) + ": " + id, options);
  }
  return res;
}
